<?php
/**
 * LEChat PHP Library
 *
 * @category    LEChat
 * @package     LEChat
 * @copyright   Copyright (c) 2011-2012 by Sascha René Leib <sascha.leib@kolmio.com>
 * @license	GPL
 * @version     $Id: library.php 0050 2011-05-21 21:52:00Z sascha $
 */
 
/**
 * Utility class for common functions.
 *
 * @package    LeChat
 */
final class LEChat {
	/**
	 * Version identification
	**/
	const VERSION = '0.5';
	
	/**
	 * SHOW DEBUG INFORMATION ?
	**/
	const DEBUGGING = false;

	/**
	 * Database storage
	**/
	const DBSERVER = 'localhost';
	const DBUSERNAME = 'username';
	const DBPASSWORD = 'password';
	const DBNAME = 'database';
	const DBTABLEPREFIX = '';
	
	/**
	 * Timeouts and other settings
	**/
	const USERIDLETIMEOUT = 10; // after how many seconds of no-request shall users turn idle
	const USEREXPIRETIMEOUT = 60; // then be marked as void
	const USERDELETETIMEOUT = 90; // and finally be deleted
	const USERUPDGRACETIME = 7; // for how many seconds shall user updates be promoted
	const MSGEXPIRATIONTIMEOUT = 600; // messages shall expire after how many seconds?
	const MSGKEEPMESSAGENUMBER = 10; // always preserve this number of messages
	
	/**
	 * GLOBALS VARIABLES
	**/
	protected $messages = array();	
	protected $db = null;
	
	/**
	 * Implementation Constructor.
	 *
	 * @return void
	**/
	private function __construct() {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = '{"dbg": {"func":"__construct"}}';
		};
	
		header('Cache-Control: no-cache, must-revalidate');
		header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
		header('Content-type: application/json');

		try {
			$this->db = mysqli_connect(self::DBSERVER, self::DBUSERNAME, self::DBPASSWORD, self::DBNAME);
			if (!$this->db->connect_errno) {
				return;
			}
		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "error": {"code": -1, "msg": "Error: ' . $e->getMessage() . '"}}';
			die();
		}
	}

	/**
	 * Implementation Destructor.
	 *
	 * @return void
	**/
	public function __destruct() {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = '{"dbg": {"func":"__destruct"}}';
		};
		
		echo "[\n " . implode(",\n ", $this->messages) . "\n]";
		$this->db = null;
	}
	
	/**
	 * initialize the user settings:
	 *
	 * @return token
	**/
	public function initUser($token, $after) {

		// assume we need a new user unless proven otherwise:
		$needNew = true;

		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = sprintf('{"dbg": {"func":"initUser", "params":{"token": "%sf", "after": "%s"}}}',
				$token,
				$after
			);
		};

		// check if token is valid & user exists:
		try {
			if ($token != "") {
				
				$query = sprintf("SELECT count(`id`) as num FROM `%susers` WHERE `token`='%s';",
					self::DBTABLEPREFIX,
					$this->db->real_escape_string($token)
				);
				if ($result = $this->db->query($query)) {
					$row = $result->fetch_assoc();
					$needNew = ((intval($row['num']) > 0) ? false : true);
					
					/* DEBUG MESSAGE: */
					if (self::DEBUGGING) {
						$this->messages[] = '{"dbg": {"func":"initUser", "event":"checked for token exists", "result":"'.$row['num'].'"}}';
					};
					$result->free_result();
				}
			}
		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "error": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
			return;
		}
		
		// do we need a new token and username?
		if ($needNew) {	

			// find a suitable username:
			/* TODO: remove dependency from COOKIE in library: */
			if (isset($_COOKIE['preferred_username'])) {
				$userName = $_COOKIE['preferred_username'];
				$suffix = '';
			} else {	
				$userName = "anon";
				$suffix = rand(101, 999);
			}

			do {
				$query = sprintf("SELECT COUNT(`id`) AS num FROM `users` WHERE `name`='%s';",
					$this->db->real_escape_string($userName . $suffix)
				);
				$result = $this->db->query($query);
				$row = $result->fetch_assoc();
				$available = (intval($row['num']) == 0);

				// get a new suffix, if necessary:
				if (!$available) {
					$suffix = rand(1000, 9999);
				}
				
				$result->free_result();
			
			} while(!$available);
			$userName .= $suffix;

			// create the new entry:
			try {
				$token = sha1($_SERVER['REMOTE_ADDR'].'-'.$userName . '-' . rand(1000000000, 999999999999));
				$query = sprintf("INSERT INTO `%susers` (name, ip, token, last_seen, status) VALUES ('%s', '%s', '%s', NOW(), 'active');",
					self::DBTABLEPREFIX,
					$this->db->real_escape_string($userName),
					$this->db->real_escape_string($_SERVER['REMOTE_ADDR']),
					$this->db->real_escape_string($token)
				);

				$result = $this->db->query($query);
			} catch (Exception $e) {
				$this->messages[] = '{"cmd":"err:msg", "error": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
			return;
			}

			// tell the client about the new username:
			$this->messages[] = sprintf('{"cmd":"init", "user": { "id":"%s", "name":"%s", "token":"%s"}}',
					$this->db->insert_id,
					htmlspecialchars($userName),
					htmlspecialchars($token)
				);
		
		} else {
		
			// if this is the first request, tell the user his name and id:
			if (intval($after) == 0) {
				$query = sprintf("SELECT `id`, `name` FROM `%susers` WHERE `token` = '%s';",
					self::DBTABLEPREFIX,
					$this->db->real_escape_string($token)
				);
				try {
					$result = $this->db->query($query);
					$row = $result->fetch_assoc();
					$this->messages[] = sprintf('{"cmd":"init", "user": { "id":"%s", "name":"%s"}}',
						$row['id'],
						htmlspecialchars($row['name'],ENT_QUOTES /* +ENT_IGNORE */)
					);
				} catch (Exception $e) {
					$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
				}
			}
			
		}
		
		// update the user status
		$this->touch($token);
		
		return $token;
	}
	
	/**
	 * Post a new message
	 *
	 * @param token $token
	 * @param String $message
	**/
	public function postMessage($token, $message, $replyTo) {

		// remove whitespaces:
		$message = trim(preg_replace("'\s+'", ' ', $message));
		
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = sprintf('{"dbg": {"func":"postMessage", "params":{"token": "%s", "message": "%s", "replyTo": "%s"}}}', $token, $message, $replyTo );
		};

		// do we need to do anything?
		if (strlen($message)<=0) {
			$this->messages[] = '{"cmd":"info", "msg": "Error: can&apos;t post empty message."}';
			return; // do nothing
		}
				
		// get the user id of the current user:
		try {
			$query = sprintf("SELECT `id`, `name` FROM `%susers` WHERE `token`='%s';",
				self::DBTABLEPREFIX,
				$this->db->real_escape_string($token)
			);
			$result = $this->db->query($query);
			$row = $result->fetch_assoc();

			// ready to create the new message setting:
			$query = sprintf("INSERT INTO `%smessages` (`userid`, `user`, `sendtime`, `message`, `replyTo`) VALUES ('%s', '%s', NOW(), '%s', '%s');",
				self::DBTABLEPREFIX,
				intval($row['id']),
				$this->db->real_escape_string($row['name']),
				$this->db->real_escape_string($message),
				intval($replyTo)
			);
			$result = $this->db->query($query);

		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
			return;
		}
	}
	
	/**
	 * Change the current username
	 *
	 * @param token $token
	 * @param string $newName
	**/
	public function changeUserName($token, $newName) {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = sprintf('{"dbg": {"func":"changeUserName", "params":{"token": "%s", "newName":"%s"}}}',
				$token,
				htmlspecialchars($newName)
			);
		};
		
		try {
			$query = sprintf("UPDATE `%susers` SET `name` = '%s', `last_updated` = NOW() WHERE `token` = '%s';",
				self::DBTABLEPREFIX,
				$this->db->real_escape_string($newName),
				$this->db->real_escape_string($token)
			);
			$result = $this->db->query($query);
		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
		}
	}
	
	/**
	 * keep the current user active
	 *
	 * @param token $token - the user token
	**/
	protected function touch($token) {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = sprintf('{"dbg": {"func":"touch", "params":{"token": "%s"}}}',
				$token
			);
		};

		try {
			$query = sprintf("UPDATE `%susers` SET `last_seen` = NOW(), `status`= 'active' WHERE `token` = '%s';",
				self::DBTABLEPREFIX,
				$this->db->real_escape_string($token)
			);
			$result = $this->db->query($query);
		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
		}
	}
	
	/**
	 * List all messages after a specific timestamp
	 *
	 * @param integer $after	The msg id after which the messags must be listed
	 * @output JSON object
	 * @return void
	**/
	public function printMessages($after) {

		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = sprintf('{"dbg": {"func":"printMessages", "params":{"after": "%s"}}}', $after );
		};
		
		$query = sprintf("SELECT `id`, `user`, `sendtime`, `message`, `replyTo` FROM `%smessages` WHERE UNIX_TIMESTAMP(`sendtime`)>%d ORDER BY `sendtime` ASC;",
			self::DBTABLEPREFIX,
			intval($after)
		);

		if($result = $this->db->query($query)) {
			while ($row = $result->fetch_assoc()) {
			
				$this->messages[] = sprintf('{"cmd":"msg:new", "message": { "id":"%s", "message":"%s", "user":"%s", "replyto":"%s", "ts":"%s"}}',
					$row['id'],
					htmlspecialchars($row['message'],ENT_QUOTES /* +ENT_IGNORE */),
					htmlspecialchars($row['user'],ENT_QUOTES /* +ENT_IGNORE */),
					$row['replyTo'],
					htmlspecialchars(strtotime($row['sendtime']),ENT_QUOTES)
				);
			}
			$result->free_result();
		}
	}
	
	/**
	 * List all users
	 *
	 * @output JSON objects
	 * @return void
	**/
	public function printUsers($printAll) {

		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = '{"dbg": {"func":"printUsers", "params":{"printAll" : "'.$printAll.'"}}}';
		};
		
		// get all the users:
		if ($printAll) {
			$query = sprintf("SELECT `id`, `name`, `ip`, `last_updated`, `status` FROM `%susers` WHERE `status`='active';",
				self::DBTABLEPREFIX
			);
		} else {
			$query = sprintf("SELECT `id`, `name`, `ip`, `last_updated`, `status` FROM `%susers` WHERE (NOW()-`last_updated`) <=%d;",
				self::DBTABLEPREFIX,
				self::USERUPDGRACETIME
			);
		}

		$result = $this->db->query($query);
		while ($row = $result->fetch_assoc()) {
		
			$this->messages[] = sprintf('{"cmd":"usr:upd", "user":{ "id":"%s", "name":"%s", "ip":"%s", "ts":"%s", "st":"%s"}}',
				$row['id'],
				htmlspecialchars($row['name'],ENT_QUOTES /* +ENT_IGNORE */),
				$row['ip'],
				htmlspecialchars(strtotime($row['last_updated']),ENT_QUOTES),
				$row['status']
			);
		}			
	}
	
	/**
	 * Housekeeping on the users list
	 * @return void
	**/
	public function usersHousekeeping($logResults = false) {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = '{"dbg": {"func":"usersHousekeeping", "params":{}}}';
		};
	
		// change user states:
		try {
			// markt users idle:
			$query = sprintf("UPDATE `%susers` SET `last_updated`=NOW(), `status`='idle' WHERE (NOW()-`last_seen`)>%d AND `status`='active';",
				self::DBTABLEPREFIX,
				self::USERIDLETIMEOUT
			);
			$this->db->query($query);
			$usersIdled = $this->db->affected_rows;

			// markt users void:
			$query = sprintf("UPDATE `%susers` SET `last_updated`=NOW(), `status`='void' WHERE (NOW()-`last_seen`)>%d AND `status`='idle';",
				self::DBTABLEPREFIX,
				self::USEREXPIRETIMEOUT
			);
			$this->db->query($query);
			$usersVoided = $this->db->affected_rows;
		
			// delete expired users:
			$query = sprintf("DELETE FROM `%susers` WHERE (NOW()-`last_seen`)>%d AND `status`='void';",
				self::DBTABLEPREFIX,
				self::USERDELETETIMEOUT
			);
			$this->db->query($query);
			$usersDeleted = $this->db->affected_rows;
		
			if ($logResults) {
				$this->messages[] = sprintf('{"log":"usr:cleanup","date":"%s","upd":{"idle":%d,"void":%d,"del":%d}}',
					htmlspecialchars(date(DATE_W3C)),
					intval($usersIdled),
					intval($usersVoided),
					intval($usersDeleted)
				);
			}

		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
		}
	}
	
	/**
	 * Housekeeping on the messages database
	 * @return void
	**/
	public function messagesHousekeeping($logResults = false) {
	
		/* DEBUG MESSAGE: */
		if (self::DEBUGGING) {
			$this->messages[] = '{"dbg": {"func":"messagesHousekeeping", "params":{}}}';
		};
		
		// clean up expired messages:
		try {
			$query = sprintf("DELETE FROM `messages` WHERE `id` <= (
					SELECT `max_keep_id` FROM (
						SELECT MAX(`id`) - %s AS `max_keep_id` FROM `messages`
					) AS `fake`
				);",
				self::MSGKEEPMESSAGENUMBER
			);
			$this->db->query($query);

			if ($logResults) {
				$messagesDeleted = $this->db->affected_rows;
				$this->messages[] = sprintf('{"log":"msg:cleanup","date":"%s","upd":{"del":%d}}',
					htmlspecialchars(date(DATE_W3C)),
					intval($messagesDeleted)
				);
			}
		} catch (Exception $e) {
			$this->messages[] = '{"cmd":"err:msg", "exception": {"code": ' . intval($e->getCode()) . ', "msg": "Exception: ' . $e->getMessage() . '"}}';
		}
	}
	
	/* STATIC METHODS */

	// singleton instance 
	private static $instance; 
	
	/**
	 * Get singleton instance
	 *
	 * @return LeChat
	**/
	public static function getInstance() { 

		if(!self::$instance) { 
			self::$instance = new self(); 
		} 
		return self::$instance; 
	}
	
	/**
	 * Get a parameter from GET, POST or possibly COOKIE
	 *
	 * @param string $token	the name of the parameter
	 * @param boolean $checkCookie	if true, the COOKIE array will be checked (optional)
	 * @return string
	**/
	public static function getParameter($name, $checkCookie = false)
	{
		return (isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : ($checkCookie && isset($_COOKIE[$name]) ? $_COOKIE[$name] : '')));
	}

	/**
	 * Check if a parameter of the given name exists
	 *
	 * @param string $token	the name of the parameter
	 * @param boolean $checkCookie	if true, the COOKIE array will be checked (optional)
	 * @return boolean
	**/
	public static function hasParameter($name, $checkCookie = false)
	{
		return ((isset($_GET[$name]) || isset($_POST[$name]) || ($checkCookie && isset($_COOKIE[$name]))) ? true : false);
	}
	
} // end of class 'LeChat'
